Skip to content

Commit dc83728

Browse files
committed
adding and removing from actionbar
1 parent d789ff1 commit dc83728

File tree

11 files changed

+468
-379
lines changed

11 files changed

+468
-379
lines changed

src/components/IconButton.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@ export const IconButton = ({ className, filled, loading, ...props }: IconButtonP
1515
const isLoading = !!loading || loading === 0
1616

1717
return (
18-
<ButtonBase
19-
className={clsx('inline-flex items-center justify-center rounded-full before:rounded-full before:bg-background-x min-w-[32px] min-h-[32px]', className)}
20-
disabled={isLoading || props.disabled}
21-
{...props}
22-
>
18+
<ButtonBase className={clsx('inline-flex items-center justify-center rounded-full', className)} disabled={isLoading || props.disabled} {...props}>
2319
{isLoading ? <CircularProgress loading={loading} /> : <Icon name={props.name} filled={filled} />}
2420
</ButtonBase>
2521
)

src/components/Select.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ export const Select = <T extends string>({
77
options,
88
className,
99
style,
10+
disabled,
1011
}: {
12+
disabled?: boolean
1113
options: { value: T; label: ReactNode; disabled?: boolean }[]
1214
value: T
1315
onChange: (v: T) => void
@@ -16,6 +18,7 @@ export const Select = <T extends string>({
1618
}) => {
1719
return (
1820
<select
21+
disabled={disabled}
1922
value={value}
2023
onChange={(e) => onChange(e.currentTarget.value as T)}
2124
className={clsx(

src/components/Toggle.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
export const Toggle = ({ value, onChange }: { value: boolean; onChange: (v: boolean) => void }) => {
1+
import clsx from 'clsx'
2+
3+
export const Toggle = ({ value, onChange, disabled }: { disabled?: boolean; value: boolean; onChange: (v: boolean) => void }) => {
24
return (
35
<label className="relative">
4-
<input type="checkbox" checked={value} onChange={(e) => onChange(e.target.checked)} className="sr-only peer" />
5-
<div className="w-9 h-5 bg-background-alt peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-primary/50 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-primary cursor-pointer"></div>
6+
<input type="checkbox" checked={value} disabled={disabled} onChange={(e) => onChange(e.target.checked)} className="sr-only peer" />
7+
<div
8+
className={clsx(
9+
"w-9 h-5 bg-background-alt peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-primary/50 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-primary ",
10+
!disabled && 'cursor-pointer',
11+
)}
12+
></div>
613
</label>
714
)
815
}

src/pages/device/ActionBar.tsx

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import { z } from 'zod'
44
import { useDeviceParams } from './useDeviceParams'
55
import { useRouteParams } from '../../utils/hooks'
66
import { IconButton } from '../../components/IconButton'
7-
import { DeviceParamType } from '../toggles/settings'
8-
import { Fragment } from 'react'
7+
import { DeviceParamType } from '../../utils/params'
98
import { useStorage } from '../../utils/storage'
109

1110
const BaseAction = z.object({
@@ -22,6 +21,7 @@ const ToggleAction = BaseAction.extend({
2221
type: z.literal('toggle'),
2322
toggleKey: z.string(),
2423
toggleType: z.number(),
24+
disabled: z.boolean().optional(),
2525
})
2626

2727
const RedirectAction = BaseAction.extend({
@@ -32,22 +32,23 @@ const RedirectAction = BaseAction.extend({
3232
export const Action = z.discriminatedUnion('type', [NavigationAction, ToggleAction, RedirectAction])
3333
export type Action = z.infer<typeof Action>
3434

35+
const BUTTON_STYLE = 'h-full w-full rounded-md border border-white/5 text-white bg-background-alt hover:bg-background'
36+
const SELECTED_BUTTON = 'bg-white !text-background-alt hover:!bg-white/80'
37+
3538
const RedirectActionComponent = ({ icon, title, href }: z.infer<typeof RedirectAction>) => {
3639
const { dongleId } = useRouteParams()
3740
return (
3841
<IconButton
3942
name={icon}
4043
href={href.replaceAll('{dongleId}', dongleId)}
4144
disabled={!href.replaceAll('{dongleId}', dongleId)}
42-
className={clsx(
43-
'text-xl flex items-center justify-center aspect-square rounded-lg bg-background-alt transition-colors border border-white/5 text-white/80 ',
44-
)}
45+
className={clsx(BUTTON_STYLE)}
4546
title={title}
4647
/>
4748
)
4849
}
4950

50-
const ToggleActionComponent = ({ icon, toggleKey, toggleType, title }: z.infer<typeof ToggleAction>) => {
51+
const ToggleActionComponent = ({ icon, toggleKey, toggleType, title, disabled }: z.infer<typeof ToggleAction>) => {
5152
const { get, isLoading, isError, save } = useDeviceParams()
5253
if (toggleType !== DeviceParamType.Boolean) return null
5354
const value = get(toggleKey as any)
@@ -58,11 +59,8 @@ const ToggleActionComponent = ({ icon, toggleKey, toggleType, title }: z.infer<t
5859
onClick={async () => {
5960
await save({ [toggleKey]: isSelected ? '0' : '1' })
6061
}}
61-
disabled={isLoading || isError || value === undefined}
62-
className={clsx(
63-
'flex items-center justify-center aspect-square rounded-lg transition-colors border border-white/5 text-xl',
64-
isSelected ? 'bg-white text-background-alt' : 'bg-background-alt text-white/80',
65-
)}
62+
disabled={isLoading || isError || value === undefined || disabled}
63+
className={clsx(BUTTON_STYLE, isSelected && SELECTED_BUTTON)}
6664
title={title}
6765
/>
6866
)
@@ -80,30 +78,36 @@ const NavigationActionComponent = ({ title, icon, location }: z.infer<typeof Nav
8078
await setMapboxRoute(address)
8179
}}
8280
disabled={!address || route === undefined}
83-
className={clsx(
84-
'flex items-center justify-center aspect-square rounded-lg transition-colors border border-white/5 text-xl',
85-
isSelected ? 'bg-white text-background-alt' : 'bg-background-alt text-white/80',
86-
)}
81+
className={clsx(BUTTON_STYLE, isSelected && SELECTED_BUTTON)}
8782
title={title}
8883
/>
8984
)
9085
}
9186

9287
export const ActionBar = ({ className }: { className?: string }) => {
93-
const [actions] = useStorage('actions')
88+
const [actions, setActions] = useStorage('actions')
89+
9490
return (
9591
<div
96-
className={clsx('grid gap-2', className)}
92+
className={clsx('flex gap-2 flex-wrap-reverse items-center justify-center', className)}
9793
style={{
98-
gridTemplateColumns: `repeat(${actions.length}, minmax(0, 1fr))`,
94+
gridTemplateColumns: `repeat(${actions.length}, minmax(2, 2fr))`,
9995
}}
10096
>
10197
{actions.map((props, i) => (
102-
<Fragment key={i}>
98+
<div key={i} className="flex text-xl relative group min-w-10 h-10 min-h-10 flex-1">
10399
{props.type === 'redirect' && <RedirectActionComponent {...props} />}
104100
{props.type === 'toggle' && <ToggleActionComponent {...props} />}
105101
{props.type === 'navigation' && <NavigationActionComponent {...props} />}
106-
</Fragment>
102+
<IconButton
103+
name="close_small"
104+
title="Remove"
105+
onClick={() => {
106+
setActions(actions.filter((_, j) => i !== j))
107+
}}
108+
className="hidden group-hover:flex absolute translate-x-1/2 -translate-y-1/2 top-0 right-0 border border-white/20 z-10 text-white bg-background aspect-square hover:bg-background-alt"
109+
/>
110+
</div>
107111
))}
108112
</div>
109113
)

src/pages/device/useDeviceParams.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { create } from 'zustand'
22
import { AthenaResponse, callAthena } from '../../api/athena'
33
import { decode, encode, parse } from '../../utils/helpers'
4-
import { DeviceParamKey } from '../toggles/settings'
4+
import { DeviceParamKey } from '../../utils/params'
55
import { toast } from 'sonner'
66

77
type DeviceParamsState = {

src/pages/toggles/Models.tsx

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { useDeviceParams } from '../device/useDeviceParams'
2+
import { Button } from '../../components/Button'
3+
import { Select } from '../../components/Select'
4+
import { useState } from 'react'
5+
import { toast } from 'sonner'
6+
import { parse } from '../../utils/helpers'
7+
8+
type ModelBundle = { index: number; display_name: string; environment: string; runner?: string; generation: number }
9+
10+
const parsePythonDict = <T,>(v: string | null | undefined): T | undefined => {
11+
if (!v) return undefined
12+
const json = v
13+
.replace(/'/g, '"')
14+
.replace(/\bTrue\b/g, 'true')
15+
.replace(/\bFalse\b/g, 'false')
16+
.replace(/\bNone\b/g, 'null')
17+
return parse(json)
18+
}
19+
20+
export const Models = () => {
21+
const { dongleId, save, get, isSaving } = useDeviceParams()
22+
const [selectedIndex, setSelectedIndex] = useState('')
23+
24+
const modelsCache = parsePythonDict<{ bundles: ModelBundle[] }>(get('ModelManager_ModelsCache'))
25+
const activeBundle = parsePythonDict<{ index: number }>(get('ModelManager_ActiveBundle'))
26+
27+
const models = modelsCache?.bundles.toReversed() ?? []
28+
const isUsingDefault = activeBundle === null
29+
const activeIndex = activeBundle?.index?.toString() ?? 'default'
30+
const selected = selectedIndex || activeIndex
31+
const selectedModel = selected === 'default' ? null : models.find((m) => m.index.toString() === selected)
32+
const isAlreadyActive = selected === activeIndex
33+
34+
const handleSend = async () => {
35+
if (isAlreadyActive || !dongleId) return
36+
const res = await save(selected === 'default' ? { ModelManager_ActiveBundle: null } : { ModelManager_DownloadIndex: selectedModel!.index.toString() })
37+
if (res?.error) toast.error(res.error.data?.message ?? res.error.message)
38+
}
39+
40+
if (!models.length) {
41+
return (
42+
<div className="flex flex-col items-center justify-center py-20 gap-2 text-center">
43+
<span className="text-lg font-medium">No models available</span>
44+
<span className="text-sm opacity-60">Model cache not found on device</span>
45+
</div>
46+
)
47+
}
48+
49+
return (
50+
<div className="flex flex-col lg:flex-row gap-6">
51+
<div className="flex-1 flex flex-col gap-4">
52+
<div className="flex flex-col gap-2">
53+
<label className="text-xs uppercase tracking-wider opacity-60">Available Models</label>
54+
<Select
55+
value={selected}
56+
onChange={setSelectedIndex}
57+
options={[{ value: 'default', label: 'Default model' }, ...models.map((m) => ({ value: m.index.toString(), label: m.display_name }))]}
58+
className="w-full"
59+
/>
60+
</div>
61+
<Button onClick={handleSend} disabled={isSaving || isAlreadyActive} className="w-full">
62+
{isSaving ? 'Sending...' : isAlreadyActive ? 'Already active' : 'Send to device'}
63+
</Button>
64+
</div>
65+
66+
<div className="flex-1 outline outline-white/10 rounded-lg p-5 bg-white/5">
67+
<p className="text-xs uppercase tracking-wider opacity-60 mb-2">Selected Model</p>
68+
{selected === 'default' ? (
69+
<>
70+
<h2 className="text-xl font-semibold">Default model</h2>
71+
<p className="text-sm opacity-60 mt-1">Uses the model bundled with openpilot</p>
72+
{isUsingDefault && <div className="mt-4 text-xs px-2 py-1 rounded-full bg-green-500/20 text-green-400 inline-block">Active</div>}
73+
</>
74+
) : selectedModel ? (
75+
<>
76+
<h2 className="text-xl font-semibold">{selectedModel.display_name}</h2>
77+
<p className="text-sm opacity-60 mt-1">{selectedModel.environment}</p>
78+
<div className="mt-4 flex flex-col gap-2 text-sm">
79+
{selectedModel.runner && (
80+
<div className="flex justify-between">
81+
<span className="opacity-60">Runner:</span>
82+
<span>{selectedModel.runner}</span>
83+
</div>
84+
)}
85+
<div className="flex justify-between">
86+
<span className="opacity-60">Generation:</span>
87+
<span>{selectedModel.generation}</span>
88+
</div>
89+
</div>
90+
{activeBundle && activeBundle.index === selectedModel.index && (
91+
<div className="mt-4 text-xs px-2 py-1 rounded-full bg-green-500/20 text-green-400 inline-block">Active</div>
92+
)}
93+
</>
94+
) : null}
95+
</div>
96+
</div>
97+
)
98+
}

0 commit comments

Comments
 (0)