mirror of
https://github.com/leon-ai/leon.git
synced 2024-12-25 17:54:43 +03:00
feat(skill/timer): custom timer component
This commit is contained in:
parent
d77c975800
commit
470889a797
1
app/src/custom-aurora-components/index.ts
Normal file
1
app/src/custom-aurora-components/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './timer'
|
1
app/src/custom-aurora-components/timer/index.ts
Normal file
1
app/src/custom-aurora-components/timer/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './timer'
|
68
app/src/custom-aurora-components/timer/timer.tsx
Normal file
68
app/src/custom-aurora-components/timer/timer.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { CircularProgress, Flexbox, Text } from '@leon-ai/aurora'
|
||||||
|
|
||||||
|
interface TimerProps {
|
||||||
|
initialTime: number
|
||||||
|
interval: number
|
||||||
|
totalTimeContent: string
|
||||||
|
onEnd?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(seconds: number): string {
|
||||||
|
const minutes = seconds >= 60 ? Math.floor(seconds / 60) : 0
|
||||||
|
const remainingSeconds = seconds % 60
|
||||||
|
const formattedMinutes = minutes < 10 ? `0${minutes}` : minutes
|
||||||
|
const formattedSeconds =
|
||||||
|
remainingSeconds < 10 ? `0${remainingSeconds}` : remainingSeconds
|
||||||
|
|
||||||
|
return `${formattedMinutes}:${formattedSeconds}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Timer({
|
||||||
|
initialTime,
|
||||||
|
interval,
|
||||||
|
totalTimeContent,
|
||||||
|
onEnd
|
||||||
|
}: TimerProps) {
|
||||||
|
const [progress, setProgress] = useState(0)
|
||||||
|
const [timeLeft, setTimeLeft] = useState(initialTime)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeLeft(initialTime)
|
||||||
|
setProgress(0)
|
||||||
|
}, [initialTime])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (timeLeft <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setTimeLeft((prevTime) => {
|
||||||
|
const newTime = prevTime - 1
|
||||||
|
|
||||||
|
if (newTime <= 0 && onEnd) {
|
||||||
|
onEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
return newTime
|
||||||
|
})
|
||||||
|
setProgress((prevProgress) => prevProgress + 100 / initialTime)
|
||||||
|
}, interval)
|
||||||
|
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}, [initialTime, interval, timeLeft])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CircularProgress value={progress} size="lg">
|
||||||
|
<Flexbox gap="xs" alignItems="center" justifyContent="center">
|
||||||
|
<Text fontSize="lg" fontWeight="semi-bold">
|
||||||
|
{formatTime(timeLeft)}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs" secondary>
|
||||||
|
{totalTimeContent}
|
||||||
|
</Text>
|
||||||
|
</Flexbox>
|
||||||
|
</CircularProgress>
|
||||||
|
)
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import { createElement } from 'react'
|
import { createElement } from 'react'
|
||||||
import * as auroraComponents from '@leon-ai/aurora'
|
import * as auroraComponents from '@leon-ai/aurora'
|
||||||
|
|
||||||
|
import * as customAuroraComponents from '../custom-aurora-components'
|
||||||
|
|
||||||
export default function renderAuroraComponent(
|
export default function renderAuroraComponent(
|
||||||
socket,
|
socket,
|
||||||
component,
|
component,
|
||||||
@ -8,7 +10,18 @@ export default function renderAuroraComponent(
|
|||||||
) {
|
) {
|
||||||
if (component) {
|
if (component) {
|
||||||
// eslint-disable-next-line import/namespace
|
// eslint-disable-next-line import/namespace
|
||||||
const reactComponent = auroraComponents[component.component]
|
let reactComponent = auroraComponents[component.component]
|
||||||
|
/**
|
||||||
|
* Find custom component if a former component is not found
|
||||||
|
*/
|
||||||
|
if (!reactComponent) {
|
||||||
|
// eslint-disable-next-line import/namespace
|
||||||
|
reactComponent = customAuroraComponents[component.component]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reactComponent) {
|
||||||
|
console.error(`Component ${component} not found`)
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the browsed component has a supported event and bind it
|
// Check if the browsed component has a supported event and bind it
|
||||||
if (
|
if (
|
||||||
|
@ -123,35 +123,6 @@ class Leon {
|
|||||||
}),
|
}),
|
||||||
supportedEvents: SUPPORTED_WIDGET_EVENTS
|
supportedEvents: SUPPORTED_WIDGET_EVENTS
|
||||||
}
|
}
|
||||||
// dynamically import the TSX component
|
|
||||||
/*const { default: tsxComponent } = await import(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-expect-error
|
|
||||||
'@@/skills/unknown/widget-playground/src/widgets/my-component.tsx'
|
|
||||||
)
|
|
||||||
console.log('tsxComponent', tsxComponent)
|
|
||||||
const componentString = ReactDOMServer.renderToString(
|
|
||||||
React.createElement(tsxComponent)
|
|
||||||
)
|
|
||||||
const componentEventHandlers = {
|
|
||||||
onClick: () => {}
|
|
||||||
}
|
|
||||||
// const componentString = ReactDOMServer.renderToString(tsxComponent)
|
|
||||||
|
|
||||||
// Collect event handlers from the component
|
|
||||||
React.Children.forEach(React.createElement(tsxComponent), (child) => {
|
|
||||||
if (child.props && child.props.onClick) {
|
|
||||||
componentEventHandlers.onClick = child.props.onClick.toString()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const response = {
|
|
||||||
componentString,
|
|
||||||
componentEventHandlers
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('componentString', componentString)
|
|
||||||
|
|
||||||
answerObject.output.widgetWithHandlers = response*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Temporize" for the data buffer output on the core
|
// "Temporize" for the data buffer output on the core
|
||||||
|
@ -7,7 +7,10 @@ interface WidgetEvent {
|
|||||||
export const SUPPORTED_WIDGET_EVENTS = [
|
export const SUPPORTED_WIDGET_EVENTS = [
|
||||||
'onClick',
|
'onClick',
|
||||||
'onSubmit',
|
'onSubmit',
|
||||||
'onChange'
|
'onChange',
|
||||||
|
'onStart',
|
||||||
|
'onEnd',
|
||||||
|
'onFetch'
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
function generateId(): string {
|
function generateId(): string {
|
||||||
|
@ -3,13 +3,20 @@ import { type WidgetWrapperProps } from '@leon-ai/aurora'
|
|||||||
import { SKILL_CONFIG } from '@bridge/constants'
|
import { SKILL_CONFIG } from '@bridge/constants'
|
||||||
import { WidgetComponent } from '@sdk/widget-component'
|
import { WidgetComponent } from '@sdk/widget-component'
|
||||||
|
|
||||||
|
type UtteranceSender = 'leon' | 'owner'
|
||||||
|
|
||||||
interface SendUtteranceWidgetEventMethodParams {
|
interface SendUtteranceWidgetEventMethodParams {
|
||||||
|
from: UtteranceSender
|
||||||
utterance: string
|
utterance: string
|
||||||
}
|
}
|
||||||
interface RunSkillActionWidgetEventMethodParams {
|
interface RunSkillActionWidgetEventMethodParams {
|
||||||
actionName: string
|
actionName: string
|
||||||
params: Record<string, unknown>
|
params: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
interface SendUtteranceOptions {
|
||||||
|
from?: UtteranceSender
|
||||||
|
data?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
export interface WidgetEventMethod {
|
export interface WidgetEventMethod {
|
||||||
methodName: 'send_utterance' | 'run_skill_action'
|
methodName: 'send_utterance' | 'run_skill_action'
|
||||||
@ -41,18 +48,20 @@ export abstract class Widget<T = unknown> {
|
|||||||
/**
|
/**
|
||||||
* Indicate the core to send a given utterance
|
* Indicate the core to send a given utterance
|
||||||
* @param key The key of the content
|
* @param key The key of the content
|
||||||
* @param data The data to apply
|
* @param options The options of the utterance
|
||||||
* @example content('provider_selected', { provider: 'Spotify' }) // 'I chose the Spotify provider'
|
* @example content('provider_selected', { data: { provider: 'Spotify' } }) // 'I chose the Spotify provider'
|
||||||
*/
|
*/
|
||||||
protected sendUtterance(
|
protected sendUtterance(
|
||||||
key: string,
|
key: string,
|
||||||
data?: Record<string, unknown>
|
options?: SendUtteranceOptions
|
||||||
): WidgetEventMethod {
|
): WidgetEventMethod {
|
||||||
const utteranceContent = this.content(key, data)
|
const utteranceContent = this.content(key, options?.data)
|
||||||
|
const from = options?.from || 'owner'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
methodName: 'send_utterance',
|
methodName: 'send_utterance',
|
||||||
methodParams: {
|
methodParams: {
|
||||||
|
from,
|
||||||
utterance: utteranceContent
|
utterance: utteranceContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,6 +102,10 @@ export abstract class Widget<T = unknown> {
|
|||||||
|
|
||||||
let content = widgetContents[key]
|
let content = widgetContents[key]
|
||||||
|
|
||||||
|
if (Array.isArray(content)) {
|
||||||
|
content = content[Math.floor(Math.random() * content.length)] as string
|
||||||
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
for (const key in data) {
|
for (const key in data) {
|
||||||
content = content.replaceAll(`%${key}%`, String(data[key]))
|
content = content.replaceAll(`%${key}%`, String(data[key]))
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"route": "/api/action/unknown/widget-playground/run",
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"route": "/api/action/news/github_trends/run",
|
"route": "/api/action/news/github_trends/run",
|
||||||
@ -11,6 +16,58 @@
|
|||||||
"route": "/api/action/news/product_hunt_trends/run",
|
"route": "/api/action/news/product_hunt_trends/run",
|
||||||
"params": []
|
"params": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"route": "/api/action/games/akinator/choose_thematic",
|
||||||
|
"params": ["thematic"],
|
||||||
|
"entitiesType": "trim"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"route": "/api/action/games/akinator/setup",
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"route": "/api/action/games/akinator/guess",
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"route": "/api/action/games/akinator/retry",
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"route": "/api/action/games/guess_the_number/setup",
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"route": "/api/action/games/guess_the_number/guess",
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"route": "/api/action/games/guess_the_number/replay",
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"route": "/api/action/games/rochambeau/start",
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"route": "/api/action/games/rochambeau/play",
|
||||||
|
"params": ["handsign"],
|
||||||
|
"entitiesType": "trim"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"route": "/api/action/games/rochambeau/rematch",
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"route": "/api/action/productivity/todo_list/create_list",
|
"route": "/api/action/productivity/todo_list/create_list",
|
||||||
@ -133,88 +190,6 @@
|
|||||||
"route": "/api/action/leon/thanks/run",
|
"route": "/api/action/leon/thanks/run",
|
||||||
"params": []
|
"params": []
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"route": "/api/action/games/akinator/choose_thematic",
|
|
||||||
"params": ["thematic"],
|
|
||||||
"entitiesType": "trim"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"route": "/api/action/games/akinator/setup",
|
|
||||||
"params": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"route": "/api/action/games/akinator/guess",
|
|
||||||
"params": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"route": "/api/action/games/akinator/retry",
|
|
||||||
"params": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"route": "/api/action/games/guess_the_number/setup",
|
|
||||||
"params": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"route": "/api/action/games/guess_the_number/guess",
|
|
||||||
"params": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"route": "/api/action/games/guess_the_number/replay",
|
|
||||||
"params": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"route": "/api/action/games/rochambeau/start",
|
|
||||||
"params": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"route": "/api/action/games/rochambeau/play",
|
|
||||||
"params": ["handsign"],
|
|
||||||
"entitiesType": "trim"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"route": "/api/action/games/rochambeau/rematch",
|
|
||||||
"params": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"route": "/api/action/unknown/widget-playground/run",
|
|
||||||
"params": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"route": "/api/action/social_communication/conversation/setup",
|
|
||||||
"params": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"route": "/api/action/social_communication/conversation/chit_chat",
|
|
||||||
"params": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"route": "/api/action/social_communication/conversation/converse",
|
|
||||||
"params": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"route": "/api/action/social_communication/mbti/setup",
|
|
||||||
"params": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"route": "/api/action/social_communication/mbti/quiz",
|
|
||||||
"params": []
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"route": "/api/action/utilities/date_time/current_date_time",
|
"route": "/api/action/utilities/date_time/current_date_time",
|
||||||
@ -305,6 +280,31 @@
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"route": "/api/action/utilities/translator-poc/translate",
|
"route": "/api/action/utilities/translator-poc/translate",
|
||||||
"params": []
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"route": "/api/action/social_communication/conversation/setup",
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"route": "/api/action/social_communication/conversation/chit_chat",
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"route": "/api/action/social_communication/conversation/converse",
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"route": "/api/action/social_communication/mbti/setup",
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"route": "/api/action/social_communication/mbti/quiz",
|
||||||
|
"params": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -180,17 +180,20 @@ export default class SocketServer {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Listen for widget events
|
// Listen for widget events
|
||||||
this.socket?.on('widget-event', (event: WidgetDataEvent) => {
|
this.socket?.on('widget-event', async (event: WidgetDataEvent) => {
|
||||||
LogHelper.title('Socket')
|
LogHelper.title('Socket')
|
||||||
LogHelper.info(`Widget event: ${JSON.stringify(event)}`)
|
LogHelper.info(`Widget event: ${JSON.stringify(event)}`)
|
||||||
|
|
||||||
const { method } = event
|
const { method } = event
|
||||||
|
|
||||||
if (method.methodName === 'send_utterance') {
|
if (method.methodName === 'send_utterance') {
|
||||||
this.socket?.emit(
|
const utterance = method.methodParams['utterance']
|
||||||
'widget-send-utterance',
|
|
||||||
method.methodParams['utterance']
|
if (method.methodParams['from'] === 'leon') {
|
||||||
)
|
await BRAIN.talk(utterance as string, true)
|
||||||
|
} else {
|
||||||
|
this.socket?.emit('widget-send-utterance', utterance)
|
||||||
|
}
|
||||||
} else if (method.methodName === 'run_skill_action') {
|
} else if (method.methodName === 'run_skill_action') {
|
||||||
this.socket?.emit('widget-run-skill-action', method.methodParams)
|
this.socket?.emit('widget-run-skill-action', method.methodParams)
|
||||||
}
|
}
|
||||||
|
@ -286,7 +286,12 @@ export const skillConfigSchemaObject = Type.Strict(
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
answers: Type.Optional(Type.Record(Type.String(), Type.Array(answerTypes))),
|
answers: Type.Optional(Type.Record(Type.String(), Type.Array(answerTypes))),
|
||||||
widget_contents: Type.Optional(Type.Record(Type.String(), Type.String())),
|
widget_contents: Type.Optional(
|
||||||
|
Type.Record(
|
||||||
|
Type.String(),
|
||||||
|
Type.Union([Type.String(), Type.Array(Type.String())])
|
||||||
|
)
|
||||||
|
),
|
||||||
entities: Type.Optional(Type.Record(Type.String(), Type.String())),
|
entities: Type.Optional(Type.Record(Type.String(), Type.String())),
|
||||||
resolvers: Type.Optional(
|
resolvers: Type.Optional(
|
||||||
Type.Record(
|
Type.Record(
|
||||||
|
@ -29,7 +29,9 @@ export class PlaygroundTestWidget extends Widget<Params> {
|
|||||||
children: provider,
|
children: provider,
|
||||||
onClick: (): WidgetEventMethod => {
|
onClick: (): WidgetEventMethod => {
|
||||||
return this.sendUtterance('choose_provider', {
|
return this.sendUtterance('choose_provider', {
|
||||||
|
data: {
|
||||||
provider
|
provider
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -59,5 +59,13 @@
|
|||||||
"Sorry, I can't set a timer for this unit. Use %hours%, %minutes% or %seconds% instead.",
|
"Sorry, I can't set a timer for this unit. Use %hours%, %minutes% or %seconds% instead.",
|
||||||
"I can't set a timer for this duration. Use %hours%, %minutes% or %seconds% instead."
|
"I can't set a timer for this duration. Use %hours%, %minutes% or %seconds% instead."
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"widget_contents": {
|
||||||
|
"second_unit": "second",
|
||||||
|
"seconds_unit": "seconds",
|
||||||
|
"minutes_unit": "minutes",
|
||||||
|
"minute_unit": "minute",
|
||||||
|
"total_time": "Total %value% %unit%",
|
||||||
|
"times_up": ["Time's up!", "The timer is up!", "The timer has ended!"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,6 @@ import { leon } from '@sdk/leon'
|
|||||||
|
|
||||||
import { TimerWidget } from '../widgets/timer'
|
import { TimerWidget } from '../widgets/timer'
|
||||||
|
|
||||||
function secondsToMinutes(seconds: number): number {
|
|
||||||
return seconds / 60
|
|
||||||
}
|
|
||||||
|
|
||||||
export const run: ActionFunction = async function (params) {
|
export const run: ActionFunction = async function (params) {
|
||||||
const supportedUnits = ['hours', 'minutes', 'seconds']
|
const supportedUnits = ['hours', 'minutes', 'seconds']
|
||||||
const durations = (
|
const durations = (
|
||||||
@ -24,12 +20,17 @@ export const run: ActionFunction = async function (params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { value: durationValue } = duration
|
const { value: durationValue } = duration
|
||||||
|
const seconds = Number(durationValue)
|
||||||
const timerWidget = new TimerWidget({
|
const timerWidget = new TimerWidget({
|
||||||
params: {
|
params: {
|
||||||
minutes: secondsToMinutes(Number(durationValue))
|
seconds
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO: return a speech without new utterance
|
||||||
|
/*await leon.answer({
|
||||||
|
widget: timerWidget,
|
||||||
|
speech: 'I set a timer for ... ...'
|
||||||
|
})*/
|
||||||
await leon.answer({ widget: timerWidget })
|
await leon.answer({ widget: timerWidget })
|
||||||
}
|
}
|
||||||
|
14
skills/utilities/timer/src/widgets/components/timer.ts
Normal file
14
skills/utilities/timer/src/widgets/components/timer.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { WidgetComponent } from '@sdk/widget-component'
|
||||||
|
|
||||||
|
interface TimerProps {
|
||||||
|
initialTime: number
|
||||||
|
interval: number
|
||||||
|
totalTimeContent: string
|
||||||
|
onEnd?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Timer extends WidgetComponent<TimerProps> {
|
||||||
|
constructor(props: TimerProps) {
|
||||||
|
super(props)
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
import { Widget, type WidgetOptions } from '@sdk/widget'
|
import { Widget, type WidgetEventMethod, type WidgetOptions } from '@sdk/widget'
|
||||||
import { type WidgetComponent } from '@sdk/widget-component'
|
import { type WidgetComponent } from '@sdk/widget-component'
|
||||||
import { Flexbox, CircularProgress, Text } from '@sdk/aurora'
|
|
||||||
|
import { Timer } from './components/timer'
|
||||||
|
|
||||||
interface Params {
|
interface Params {
|
||||||
minutes: number
|
seconds: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TimerWidget extends Widget<Params> {
|
export class TimerWidget extends Widget<Params> {
|
||||||
@ -15,30 +16,41 @@ export class TimerWidget extends Widget<Params> {
|
|||||||
* TODO
|
* TODO
|
||||||
* 1. Save timer + timer id in memory
|
* 1. Save timer + timer id in memory
|
||||||
* 2. On rendering, set widget id to timer id
|
* 2. On rendering, set widget id to timer id
|
||||||
* 3. When load feed, need to fetch all timers as per their timer id. Need a built-in API here
|
* 3. When load feed, need to fetch all timers (onFetch?) as per their timer id. Need a built-in API here
|
||||||
|
* 4. onEnd (or onChange and check if done?), then trigger next action or utterance
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public render(): WidgetComponent {
|
public render(): WidgetComponent {
|
||||||
return new CircularProgress({
|
const { seconds } = this.params
|
||||||
value: 0,
|
const secondUnitContent = this.content('second_unit')
|
||||||
size: 'lg',
|
const secondsUnitContent = this.content('seconds_unit')
|
||||||
children: new Flexbox({
|
const minuteUnitContent = this.content('minute_unit')
|
||||||
gap: 'xs',
|
const minutesUnitContent = this.content('minutes_unit')
|
||||||
alignItems: 'center',
|
let totalTimeContent = ''
|
||||||
justifyContent: 'center',
|
|
||||||
children: [
|
if (seconds >= 60) {
|
||||||
new Text({
|
const minutes = seconds / 60
|
||||||
fontSize: 'lg',
|
|
||||||
fontWeight: 'semi-bold',
|
totalTimeContent = this.content('total_time', {
|
||||||
children: 0
|
value: minutes % 1 === 0 ? minutes : minutes.toFixed(2),
|
||||||
}),
|
unit: minutes > 1 ? minutesUnitContent : minuteUnitContent
|
||||||
new Text({
|
|
||||||
fontSize: 'xs',
|
|
||||||
secondary: true,
|
|
||||||
children: 'Total 10 minutes'
|
|
||||||
})
|
})
|
||||||
]
|
} else {
|
||||||
|
totalTimeContent = this.content('total_time', {
|
||||||
|
value: seconds,
|
||||||
|
unit: seconds > 1 ? secondsUnitContent : secondUnitContent
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Timer({
|
||||||
|
initialTime: seconds,
|
||||||
|
interval: 1_000,
|
||||||
|
totalTimeContent,
|
||||||
|
onEnd: (): WidgetEventMethod => {
|
||||||
|
return this.sendUtterance('times_up', {
|
||||||
|
from: 'leon'
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
|
"jsx": "react",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@@/*": ["./*"],
|
"@@/*": ["./*"],
|
||||||
"@/*": ["./server/src/*"],
|
"@/*": ["./server/src/*"],
|
||||||
|
Loading…
Reference in New Issue
Block a user